Erkunden Sie die Feinheiten des WebGL GPU-Befehlspuffers. Lernen Sie, wie Sie die Rendering-Leistung durch Low-Level-Grafikbefehlsaufzeichnung und -ausführung optimieren.
Den WebGL GPU-Befehlspuffer beherrschen: Ein tiefer Einblick in die Low-Level-Grafikaufzeichnung
In der Welt der Webgrafik arbeiten wir oft mit High-Level-Bibliotheken wie Three.js oder Babylon.js, die die Komplexität der zugrunde liegenden Rendering-APIs abstrahieren. Um jedoch wirklich die maximale Leistung freizusetzen und zu verstehen, was unter der Haube geschieht, müssen wir die Schichten abtragen. Im Herzen jeder modernen Grafik-API – einschließlich WebGL – liegt ein grundlegendes Konzept: der GPU-Befehlspuffer.
Das Verständnis des Befehlspuffers ist nicht nur eine akademische Übung. Es ist der Schlüssel zur Diagnose von Leistungsengpässen, zum Schreiben von hocheffizientem Rendering-Code und zum Begreifen des architektonischen Wandels hin zu neueren APIs wie WebGPU. Dieser Artikel nimmt Sie mit auf einen tiefen Einblick in den WebGL-Befehlspuffer, erforscht seine Rolle, seine Auswirkungen auf die Leistung und wie eine befehlszentrierte Denkweise Sie in einen effektiveren Grafikprogrammierer verwandeln kann.
Was ist der GPU-Befehlspuffer? Ein Überblick
Im Kern ist ein GPU-Befehlspuffer ein Speicherbereich, der eine sequenzielle Liste von Befehlen für die Graphics Processing Unit (GPU) zur Ausführung speichert. Wenn Sie in Ihrem JavaScript-Code einen WebGL-Aufruf tätigen, wie gl.drawArrays() oder gl.clear(), weisen Sie die GPU nicht direkt an, etwas sofort zu tun. Stattdessen beauftragen Sie die Grafik-Engine des Browsers, einen entsprechenden Befehl in einem Puffer aufzuzeichnen.
Stellen Sie sich die Beziehung zwischen der CPU (die Ihr JavaScript ausführt) und der GPU (die die Grafik rendert) wie die eines Generals und eines Soldaten auf einem Schlachtfeld vor. Die CPU ist der General, der die gesamte Operation strategisch plant. Sie schreibt eine Reihe von Befehlen auf – 'schlage hier ein Lager auf', 'binde diese Textur', 'zeichne diese Dreiecke', 'aktiviere den Tiefentest'. Diese Liste von Befehlen ist der Befehlspuffer.
Sobald die Liste für einen bestimmten Frame vollständig ist, 'übermittelt' die CPU diesen Puffer an die GPU. Die GPU, der fleißige Soldat, nimmt die Liste entgegen und führt die Befehle einen nach dem anderen aus, völlig unabhängig von der CPU. Diese asynchrone Architektur ist die Grundlage moderner Hochleistungsgrafik. Sie ermöglicht es der CPU, mit der Vorbereitung der Befehle für den nächsten Frame fortzufahren, während die GPU mit der Arbeit am aktuellen beschäftigt ist, wodurch eine parallele Verarbeitungspipeline entsteht.
In WebGL ist dieser Prozess weitgehend implizit. Sie tätigen API-Aufrufe, und der Browser sowie der Grafiktreiber verwalten die Erstellung und Übermittlung des Befehlspuffers für Sie. Dies steht im Gegensatz zu neueren APIs wie WebGPU oder Vulkan, bei denen Entwickler explizite Kontrolle über das Erstellen, Aufzeichnen und Übermitteln von Befehlspuffern haben. Die zugrunde liegenden Prinzipien sind jedoch identisch, und ihr Verständnis im Kontext von WebGL ist entscheidend für die Leistungsoptimierung.
Der Weg eines Draw Calls: Von JavaScript zu Pixeln
Um den Befehlspuffer wirklich wertzuschätzen, verfolgen wir den Lebenszyklus eines typischen Rendering-Frames. Es ist eine mehrstufige Reise, die die Grenze zwischen der CPU- und GPU-Welt mehrfach überquert.
1. Die CPU-Seite: Ihr JavaScript-Code
Alles beginnt in Ihrer JavaScript-Anwendung. Innerhalb Ihrer requestAnimationFrame-Schleife geben Sie eine Reihe von WebGL-Aufrufen aus, um Ihre Szene zu rendern. Zum Beispiel:
function render(time) {
// 1. Globalen Zustand einrichten
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.clearColor(0.1, 0.2, 0.3, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.enable(gl.DEPTH_TEST);
// 2. Ein bestimmtes Shader-Programm verwenden
gl.useProgram(myShaderProgram);
// 3. Puffer binden und Uniforms für ein Objekt setzen
gl.bindVertexArray(myObjectVAO);
gl.uniformMatrix4fv(locationOfModelViewMatrix, false, modelViewMatrix);
gl.uniformMatrix4fv(locationOfProjectionMatrix, false, projectionMatrix);
// 4. Den Zeichenbefehl ausgeben
const primitiveType = gl.TRIANGLES;
const offset = 0;
const count = 36; // z.B. für einen Würfel
gl.drawArrays(primitiveType, offset, count);
requestAnimationFrame(render);
}
Entscheidend ist, dass keiner dieser Aufrufe ein sofortiges Rendering bewirkt. Jeder Funktionsaufruf, wie gl.useProgram oder gl.uniformMatrix4fv, wird in einen oder mehrere Befehle übersetzt, die im internen Befehlspuffer des Browsers eingereiht werden. Sie erstellen lediglich das Rezept für den Frame.
2. Die Treiberseite: Übersetzung und Validierung
Die WebGL-Implementierung des Browsers fungiert als Zwischenschicht. Sie nimmt Ihre hochrangigen JavaScript-Aufrufe entgegen und führt mehrere wichtige Aufgaben aus:
- Validierung: Sie prüft, ob Ihre API-Aufrufe gültig sind. Haben Sie ein Programm gebunden, bevor Sie eine Uniform gesetzt haben? Liegen die Puffer-Offsets und -Zählungen in gültigen Bereichen? Deshalb erhalten Sie Konsolenfehler wie
"WebGL: INVALID_OPERATION: useProgram: program not valid". Dieser Validierungsschritt schützt die GPU vor ungültigen Befehlen, die einen Absturz oder Systeminstabilität verursachen könnten. - Zustandsverfolgung: WebGL ist eine Zustandsmaschine. Der Treiber verfolgt den aktuellen Zustand (welches Programm aktiv ist, welche Textur an Einheit 0 gebunden ist usw.), um redundante Befehle zu vermeiden.
- Übersetzung: Die validierten WebGL-Aufrufe werden in die native Grafik-API des zugrunde liegenden Betriebssystems übersetzt. Dies kann DirectX unter Windows, Metal unter macOS/iOS oder OpenGL/Vulkan unter Linux und Android sein. Die Befehle werden in diesem nativen Format in einem treiberseitigen Befehlspuffer eingereiht.
3. Die GPU-Seite: Asynchrone Ausführung
An einem bestimmten Punkt, typischerweise am Ende der JavaScript-Aufgabe, die Ihre Render-Schleife ausmacht, wird der Browser den Befehlspuffer leeren (flush). Das bedeutet, er nimmt den gesamten Stapel aufgezeichneter Befehle und übermittelt ihn an den Grafiktreiber, der ihn wiederum an die GPU-Hardware weitergibt.
Die GPU holt dann Befehle aus ihrer Warteschlange und beginnt mit deren Ausführung. Ihre hochparallele Architektur ermöglicht es ihr, Vertices im Vertex-Shader zu verarbeiten, Dreiecke in Fragmente zu rastern und den Fragment-Shader auf Millionen von Pixeln gleichzeitig auszuführen. Während dies geschieht, ist die CPU bereits frei, mit der Verarbeitung der Logik für den nächsten Frame zu beginnen – Physik berechnen, KI ausführen und den nächsten Befehlspuffer erstellen. Diese Entkopplung ermöglicht ein flüssiges Rendering mit hoher Bildrate.
Jede Operation, die diese Parallelität unterbricht, wie z.B. das Anfordern von Daten von der GPU (z.B. gl.readPixels()), zwingt die CPU, darauf zu warten, dass die GPU ihre Arbeit beendet. Dies wird als CPU-GPU-Synchronisation oder Pipeline-Stall bezeichnet und ist eine Hauptursache für Leistungsprobleme.
Im Inneren des Puffers: Von welchen Befehlen sprechen wir?
Ein GPU-Befehlspuffer ist kein monolithischer Block aus unentzifferbarem Code. Es ist eine strukturierte Abfolge von verschiedenen Operationen, die in mehrere Kategorien fallen. Das Verständnis dieser Kategorien ist der erste Schritt zur Optimierung ihrer Erzeugung.
-
Zustandssetzende Befehle: Diese Befehle konfigurieren die Fixed-Function-Pipeline und die programmierbaren Stufen der GPU. Sie zeichnen nichts direkt, sondern definieren wie nachfolgende Zeichenbefehle ausgeführt werden. Beispiele sind:
gl.useProgram(program): Setzt die aktiven Vertex- und Fragment-Shader.gl.enable() / gl.disable(): Schaltet Funktionen wie Tiefentest, Blending oder Culling ein oder aus.gl.viewport(x, y, w, h): Definiert den Bereich des Framebuffers, in den gerendert wird.gl.depthFunc(func): Legt die Bedingung für den Tiefentest fest (z.B.gl.LESS).gl.blendFunc(sfactor, dfactor): Konfiguriert, wie Farben für Transparenz gemischt werden.
-
Ressourcenbindungsbefehle: Diese Befehle verbinden Ihre Daten (Meshes, Texturen, Uniforms) mit den Shader-Programmen. Die GPU muss wissen, wo sie die zu verarbeitenden Daten finden kann.
gl.bindBuffer(target, buffer): Bindet einen Vertex- oder Index-Puffer.gl.bindTexture(target, texture): Bindet eine Textur an eine aktive Textureinheit.gl.bindFramebuffer(target, fb): Legt das Render-Ziel fest.gl.uniform*(): Lädt Uniform-Daten (wie Matrizen oder Farben) in das aktuelle Shader-Programm hoch.gl.vertexAttribPointer(): Definiert das Layout der Vertex-Daten innerhalb eines Puffers. (Oft in einem Vertex Array Object, oder VAO, gekapselt).
-
Zeichenbefehle: Dies sind die Aktionsbefehle. Sie sind diejenigen, die die GPU tatsächlich veranlassen, die Rendering-Pipeline zu starten, indem sie den aktuell gebundenen Zustand und die Ressourcen verbrauchen, um Pixel zu erzeugen.
gl.drawArrays(mode, first, count): Rendert Primitive aus Array-Daten.gl.drawElements(mode, count, type, offset): Rendert Primitive unter Verwendung eines Index-Puffers.gl.drawArraysInstanced() / gl.drawElementsInstanced(): Rendert mehrere Instanzen derselben Geometrie mit einem einzigen Befehl.
-
Löschbefehle: Eine spezielle Art von Befehl, der verwendet wird, um die Farb-, Tiefen- oder Schablonenpuffer des Framebuffers zu löschen, typischerweise am Anfang eines Frames.
gl.clear(mask): Löscht den aktuell gebundenen Framebuffer.
Die Bedeutung der Befehlsreihenfolge
Die GPU führt diese Befehle in der Reihenfolge aus, in der sie im Puffer erscheinen. Diese sequentielle Abhängigkeit ist entscheidend. Sie können keinen gl.drawArrays-Befehl ausgeben und erwarten, dass er korrekt funktioniert, ohne zuvor den notwendigen Zustand eingestellt zu haben. Die korrekte Reihenfolge ist immer: Zustand setzen -> Ressourcen binden -> Zeichnen. Das Vergessen von gl.useProgram vor dem Setzen seiner Uniforms oder dem Zeichnen damit ist ein häufiger Fehler bei Anfängern. Das mentale Modell sollte sein: 'Ich bereite den Kontext der GPU vor, dann sage ich ihr, sie soll eine Aktion innerhalb dieses Kontexts ausführen'.
Optimierung für den Befehlspuffer: Von Gut zu Großartig
Jetzt kommen wir zum praktischsten Teil unserer Diskussion. Wenn es bei der Leistung einfach darum geht, eine effiziente Liste von Befehlen für die GPU zu erstellen, wie machen wir das? Das Kernprinzip ist einfach: Machen Sie der GPU die Arbeit leicht. Das bedeutet, ihr weniger, aber aussagekräftigere Befehle zu senden und Aufgaben zu vermeiden, die sie zum Anhalten und Warten zwingen.
1. Minimierung von Zustandsänderungen
Das Problem: Jeder zustandssetzende Befehl (gl.useProgram, gl.bindTexture, gl.enable) ist eine Anweisung im Befehlspuffer. Während einige Zustandsänderungen günstig sind, können andere teuer sein. Das Ändern eines Shader-Programms kann beispielsweise erfordern, dass die GPU ihre internen Pipelines leert und einen neuen Satz von Anweisungen lädt. Ständiges Wechseln der Zustände zwischen den Draw Calls ist, als würde man einen Fabrikarbeiter bitten, seine Maschine für jedes einzelne Produkt, das er herstellt, neu zu rüsten – es ist unglaublich ineffizient.
Die Lösung: Render-Sortierung (oder Batching nach Zustand)
Die leistungsstärkste Optimierungstechnik hierbei ist das Gruppieren Ihrer Draw Calls nach ihrem Zustand. Anstatt Ihre Szene Objekt für Objekt in der Reihenfolge ihres Erscheinens zu rendern, strukturieren Sie Ihre Render-Schleife so um, dass alle Objekte, die dasselbe Material (Shader, Texturen, Blend-Zustand) teilen, zusammen gerendert werden.
Betrachten Sie eine Szene mit zwei Shadern (Shader A und Shader B) und vier Objekten:
Ineffizienter Ansatz (Objekt für Objekt):
- Shader A verwenden
- Ressourcen für Objekt 1 binden
- Objekt 1 zeichnen
- Shader B verwenden
- Ressourcen für Objekt 2 binden
- Objekt 2 zeichnen
- Shader A verwenden
- Ressourcen für Objekt 3 binden
- Objekt 3 zeichnen
- Shader B verwenden
- Ressourcen für Objekt 4 binden
- Objekt 4 zeichnen
Dies führt zu 4 Shader-Wechseln (useProgram-Aufrufen).
Effizienter Ansatz (Nach Shader sortiert):
- Shader A verwenden
- Ressourcen für Objekt 1 binden
- Objekt 1 zeichnen
- Ressourcen für Objekt 3 binden
- Objekt 3 zeichnen
- Shader B verwenden
- Ressourcen für Objekt 2 binden
- Objekt 2 zeichnen
- Ressourcen für Objekt 4 binden
- Objekt 4 zeichnen
Dies führt zu nur 2 Shader-Wechseln. Die gleiche Logik gilt für Texturen, Blend-Modi und andere Zustände. Hochleistungs-Renderer verwenden oft einen mehrstufigen Sortierschlüssel (z.B. nach Transparenz, dann nach Shader, dann nach Textur sortieren), um Zustandsänderungen so weit wie möglich zu minimieren.
2. Reduzierung von Draw Calls (Batching nach Geometrie)
Das Problem: Jeder Draw Call (gl.drawArrays, gl.drawElements) bringt einen gewissen CPU-Overhead mit sich. Der Browser muss den Aufruf validieren, aufzeichnen und der Treiber muss ihn verarbeiten. Das Ausgeben von Tausenden von Draw Calls für winzige Objekte kann die CPU schnell überfordern, sodass die GPU auf Befehle warten muss. Dies wird als CPU-gebunden bezeichnet.
Die Lösungen:
- Statisches Batching: Wenn Sie viele kleine, statische Objekte in Ihrer Szene haben, die dasselbe Material teilen (z.B. Bäume in einem Wald, Nieten an einer Maschine), kombinieren Sie deren Geometrie vor Beginn des Renderings in einem einzigen, großen Vertex Buffer Object (VBO). Anstatt 1000 Bäume mit 1000 Draw Calls zu zeichnen, zeichnen Sie ein riesiges Mesh aus 1000 Bäumen mit einem einzigen Draw Call. Dies reduziert den CPU-Overhead drastisch.
- Instancing: Dies ist die führende Technik zum Zeichnen vieler Kopien desselben Meshes. Mit
gl.drawElementsInstancedstellen Sie eine Kopie der Mesh-Geometrie und einen separaten Puffer mit Daten pro Instanz (wie Position, Rotation, Farbe) bereit. Sie geben dann einen einzigen Draw Call aus, der der GPU mitteilt: "Zeichne dieses Mesh N-mal und verwende für jede Kopie die entsprechenden Daten aus dem Instanz-Puffer." Dies ist perfekt für das Rendern von Partikelsystemen, Menschenmengen oder Wäldern.
3. Verstehen und Vermeiden von Puffer-Flushes
Das Problem: Wie erwähnt, arbeiten CPU und GPU parallel. Die CPU füllt den Befehlspuffer, während die GPU ihn leert. Einige WebGL-Funktionen zwingen diese Parallelität jedoch zum Bruch. Funktionen wie gl.readPixels() oder gl.finish() erfordern ein Ergebnis von der GPU. Um dieses Ergebnis zu liefern, muss die GPU alle anstehenden Befehle in ihrer Warteschlange beenden. Die CPU, die die Anfrage gestellt hat, muss dann anhalten und warten, bis die GPU aufgeholt und die Daten geliefert hat. Dieser Pipeline-Stall kann Ihre Bildrate zerstören.
Die Lösung: Vermeiden Sie synchrone Operationen
- Verwenden Sie niemals
gl.readPixels(),gl.getParameter()odergl.checkFramebufferStatus()innerhalb Ihrer Haupt-Render-Schleife. Dies sind leistungsstarke Debugging-Tools, aber sie sind Performance-Killer. - Wenn Sie unbedingt Daten von der GPU zurücklesen müssen (z.B. für GPU-basiertes Picking oder Berechnungsaufgaben), verwenden Sie asynchrone Mechanismen wie Pixel Buffer Objects (PBOs) oder die Sync-Objekte von WebGL 2, die es Ihnen ermöglichen, eine Datenübertragung zu initiieren, ohne sofort auf deren Abschluss zu warten.
4. Effizienter Daten-Upload und -Verwaltung
Das Problem: Das Hochladen von Daten auf die GPU mit gl.bufferData() oder gl.texImage2D() ist ebenfalls ein Befehl, der aufgezeichnet wird. Das Senden großer Datenmengen von der CPU zur GPU in jedem Frame kann den Kommunikationsbus zwischen ihnen (typischerweise PCIe) überlasten.
Die Lösung: Planen Sie Ihre Datenübertragungen
- Statische Daten: Für Daten, die sich nie ändern (z.B. statische Modellgeometrie), laden Sie diese einmal bei der Initialisierung mit
gl.STATIC_DRAWhoch und belassen Sie sie auf der GPU. - Dynamische Daten: Für Daten, die sich in jedem Frame ändern (z.B. Partikelpositionen), weisen Sie den Puffer einmal mit
gl.bufferDataund einemgl.DYNAMIC_DRAW- odergl.STREAM_DRAW-Hinweis zu. Aktualisieren Sie dann in Ihrer Render-Schleife dessen Inhalt mitgl.bufferSubData. Dies vermeidet den Overhead der Neuzuweisung von GPU-Speicher in jedem Frame.
Die Zukunft ist explizit: WebGLs Befehlspuffer vs. WebGPUs Befehls-Encoder
Das Verständnis des impliziten Befehlspuffers in WebGL bietet die perfekte Grundlage, um die nächste Generation der Webgrafik zu würdigen: WebGPU.
Während WebGL den Befehlspuffer vor Ihnen verbirgt, macht WebGPU ihn zu einem erstklassigen Bürger der API. Dies gewährt Entwicklern ein revolutionäres Maß an Kontrolle und Leistungspotenzial.
WebGL: Das implizite Modell
In WebGL ist der Befehlspuffer eine Blackbox. Sie rufen Funktionen auf, und der Browser gibt sein Bestes, sie effizient aufzuzeichnen. All diese Arbeit muss im Hauptthread stattfinden, da der WebGL-Kontext an ihn gebunden ist. Dies kann in komplexen Anwendungen zu einem Engpass werden, da die gesamte Rendering-Logik mit UI-Updates, Benutzereingaben und anderen JavaScript-Aufgaben konkurriert.
WebGPU: Das explizite Modell
In WebGPU ist der Prozess explizit und weitaus leistungsfähiger:
- Sie erstellen ein
GPUCommandEncoder-Objekt. Dies ist Ihr persönlicher Befehlsrekorder. - Sie beginnen einen 'Durchlauf' (Pass) (z.B. einen
GPURenderPassEncoder), der Render-Ziele und Löschwerte festlegt. - Innerhalb des Durchlaufs zeichnen Sie Befehle wie
setPipeline(),setVertexBuffer()unddraw()auf. Dies fühlt sich sehr ähnlich an wie das Tätigen von WebGL-Aufrufen. - Sie rufen
.finish()auf dem Encoder auf, was ein vollständiges, opaquesGPUCommandBuffer-Objekt zurückgibt. - Schließlich übermitteln Sie ein Array dieser Befehlspuffer an die Warteschlange des Geräts:
device.queue.submit([commandBuffer]).
Diese explizite Kontrolle eröffnet mehrere bahnbrechende Vorteile:
- Multi-threaded Rendering: Da Befehlspuffer vor der Übermittlung nur Datenobjekte sind, können sie auf separaten Web Workern erstellt und aufgezeichnet werden. Sie können mehrere Worker parallel verschiedene Teile Ihrer Szene vorbereiten lassen (z.B. einen für Schatten, einen für opake Objekte, einen für die Benutzeroberfläche). Dies kann die Last auf dem Hauptthread drastisch reduzieren, was zu einer viel flüssigeren Benutzererfahrung führt.
- Wiederverwendbarkeit: Sie können einen Befehlspuffer für einen statischen Teil Ihrer Szene (oder sogar nur für ein einzelnes Objekt) vorab aufzeichnen und denselben Puffer dann in jedem Frame erneut übermitteln, ohne die Befehle neu aufzeichnen zu müssen. Dies ist in WebGPU als Render Bundle bekannt und ist unglaublich effizient für statische Geometrie.
- Reduzierter Overhead: Ein Großteil der Validierungsarbeit wird während der Aufzeichnungsphase in den Worker-Threads erledigt. Die endgültige Übermittlung im Hauptthread ist eine sehr leichtgewichtige Operation, was zu einem vorhersagbareren und geringeren CPU-Overhead pro Frame führt.
Indem Sie lernen, über den impliziten Befehlspuffer in WebGL nachzudenken, bereiten Sie sich perfekt auf die explizite, multi-threaded und hochleistungsfähige Welt von WebGPU vor.
Fazit: In Befehlen denken
Der GPU-Befehlspuffer ist das unsichtbare Rückgrat von WebGL. Auch wenn Sie vielleicht nie direkt damit interagieren, läuft jede Leistungsentscheidung, die Sie treffen, letztendlich darauf hinaus, wie effizient Sie diese Liste von Anweisungen für die GPU erstellen.
Fassen wir die wichtigsten Erkenntnisse zusammen:
- WebGL-API-Aufrufe werden nicht sofort ausgeführt; sie zeichnen Befehle in einem Puffer auf.
- CPU und GPU sind für die parallele Arbeit konzipiert. Ihr Ziel ist es, beide beschäftigt zu halten, ohne dass eine auf die andere warten muss.
- Leistungsoptimierung ist die Kunst, einen schlanken und effizienten Befehlspuffer zu erzeugen.
- Die wirkungsvollsten Strategien sind die Minimierung von Zustandsänderungen durch Render-Sortierung und die Reduzierung von Draw Calls durch Geometrie-Batching und Instancing.
- Das Verständnis dieses impliziten Modells in WebGL ist der Schlüssel zur Beherrschung der expliziten, leistungsfähigeren Befehlspuffer-Architektur moderner APIs wie WebGPU.
Wenn Sie das nächste Mal Rendering-Code schreiben, versuchen Sie, Ihr mentales Modell zu ändern. Denken Sie nicht nur: "Ich rufe eine Funktion auf, um ein Mesh zu zeichnen." Denken Sie stattdessen: "Ich füge eine Reihe von Zustands-, Ressourcen- und Zeichenbefehlen zu einer Liste hinzu, die die GPU schließlich ausführen wird." Diese befehlszentrierte Perspektive ist das Kennzeichen eines fortgeschrittenen Grafikprogrammierers und der Schlüssel, um das volle Potenzial der Ihnen zur Verfügung stehenden Hardware freizusetzen.